(ns net.tcp.server
  "直接调用java的ServerSocket服务端"
  (:require [clojure.java.io :as io])
  (:import [java.net InetAddress ServerSocket SocketException]))

(defn- server-socket [server]
  "创建一个tcp服务器"
  (ServerSocket.
    (:port server)
    (:backlog server)
    (InetAddress/getByName (:host server))))

(defn tcp-server
  "创建一个tcp服务器 需要如下参数
    :host 绑定的主机ip
    :port 绑定的端口
    :handler 连接处理函数
    :backlog 队列数量"
  [& {:as options}]
  ;; 判断选项一定更要包含端口和处理函数
  {:pre [(:port options)
         (:handler options)]}
  (merge
    {:host        "127.0.0.1"
     :backlog     50
     :socket      (atom nil)
     :connections (atom #{})}
    options))

(defn close-socket [server socket]
  "关闭socket"
  ;; 在原来的链接集合中去掉选中的socket
  (swap! (:connections server) disj socket)
  ;; 当socket还没有关闭时,关闭套接字
  (when-not (.isClosed socket) (.close socket)))

(defn- open-server-socket [server]
  ;; 打开服务端socket 如果服务端套接字存在 那么替换它
  (reset! (:socket server)
          (server-socket server)))

(defn- accept-connection
  [{:keys [handler connections socket] :as server}]
  ;; 监听并接受此套接字的连接
  (let [conn (.accept @socket)]
    ;; 代码会阻塞在上面一步
    ;; 有连接时 把连接增加到链接结合中
    (swap! connections conj conn)
    ;; 开一个线程去处理该连接
    ;; 并且直接在该线程中返回相关数据
    ;; 关闭链接
    (future
      (try (handler conn)
           (finally (close-socket server conn))))))

(defn running?
  "判断服务器是否已经启动"
  [server]
  (if-let [socket @(:socket server)]
    (not (.isClosed socket))))

(defn start
  "开启一个tcp服务器"
  [server]
  (open-server-socket server)
  ;; 开启一个线程去监听服务端socket是否有新连接加入
  ;; 然后有新连接加入时 再开启一个新的连接去处理那个新连接
  (future
    (while (running? server)
      (try
        (accept-connection server)
        (catch SocketException _)))))

(defn stop
  "停止服务器并且关闭所有链接"
  [server]
  ;; 遍历所有链接 关闭它们 从集合中逐个去掉
  (doseq [socket @(:connections server)]
    (close-socket server socket))
  (.close @(:socket server)))

(defn wrap-streams
  "包裹一层处理器 把期望的参数从socket改成输入输出"
  [handler]
  ;; 把函数抽象成需要输入/输出流的抽象 便于做适配器
  (fn [socket]
    (with-open [input (.getInputStream socket)
                output (.getOutputStream socket)]
      (handler input output))))

(defn wrap-io
  "包裹一层处理器 把期望的参数从input output 改成输入或者输出"
  [handler]
  ;; 把函数抽象成需要标准clojure.io中的输入输出
  ;; 这样子就可以把clojure和java很优雅的黏贴在一起
  (wrap-streams
    (fn [input output]
      (with-open [reader (io/reader input)
                  writer (io/writer output)]
        (handler reader writer)))))